请注意这是 libev 而不是 libevent 的文章!
这篇文章是第三篇,主要讲 libev 里基本集中的 watcher。
本文地址:https://segmentfault.com/a/1190000006679929
ev_io:直接操作fd
这个 watcher 负责检测文件描述符(以下简称fd)是否可写入数据或者是读出数据。最好是将fd设置为非阻塞的。
注意有时候在调用read
时是没有数据的(返回0),此时一个一个非阻塞的read
会得到EAGAIN
错误。
(以下两个特殊问题,是 libev 文档中特别提到的,但是我看不太懂……)
失踪的 fd 的特殊问题
部分系统需要显式地调用close
(如kqueue
、epoll
),否则当一个 fd 消失、而新的 fd 进入,占用同一个 fd 号时,libev
不知道这是一个新的fd。
libev 一侧解决的办法是每次调用ev_io_set
时,都假定这是一个新的 fd。
使用dup
操作 fd 的特殊问题
一些后端(backend)不能注册普通的 fd 事件,只能注册underlying file descriptions
,这意味着使用dup()
或其他奇怪操作的fd,只能由其中一个被接收到。
这没有有效的解决办法,除非将后端设置为BACKEND_SELECT
或EVBACKEND_POLL
关于文件的特殊问题
ev_io
对于文件泪说没有什么用,只要文件存在,就立即会有时间。对于stdin
和stdout
,请谨慎使用,确保这两者没有被重定向至文件。
关于 fork 的特殊问题
记得使用ev_loop_fork
,并且使用EVFLAG_FORKCHECK
。不过对于epoll
和kqueue
之外的无需担心。
关于SIGPIPE的问题
只是提醒一下:记得处理SIGPIPE
事件。
关于accept
一个无法接受的连接
大多数 POSIX accpet 实现中在删除因为错误而导致的连接时(如 fd 到达上限)都回产生一个错误的操作,比如使 accept 失败但不拒绝连接,只产生ENFILE
错误。但这个会导致 libev 还是将其标记为 ready 状态。
推荐方法是列出所有的错误并记录下来,或者是暂时关闭 watchers。
相关函数
void ev_io_init (ev_io *, callback, int fd, int events)
void ev_io_set (ev)io *, int fd, int events)
其中 events 可以是EV_WRITE
和EV_READ
的组合。
示例
static void stdin_readable_db (struct ev_loop *loop,
ev_io *w,
int revents)
{
ev_io_stop (loop, w)
...... // 从 w->fd 中进行read
}
......
some_init_func ()
{
......
struct ev_loop *loop = ev_default_init (0);
ev_io stdin_readable;
ev_io_init (&stdin_readable, stdin_readable_db , STDIN_FILENO, EV_READ);
ev_io_start (loop, &stdin_readable);
ev_run (loop, 0);
...
}
ev_timer:相对超时机制
Libev 提供了一个相对超时机制的定时器。所谓的“相对”,就是说这个定时器的参数是:指定以当前时间为基准,延迟多久出发事件。这个定时器与基于万年历的日期/时间是无关的,只基于系统单调时间。
循环定时器设计
下面列出一个以60秒为单位的循环定时器作为例子,来说明使用ev_timer的不同策略
1. 使用标准的初始化和停止 API 来重设
ev_timer_init (timer, callback, 60.0, 6.0);
ev_timer_start (loop, timer)
标准设置。或——
ev_timer_stop (loop, timer);
ev_timer_set (timer, 60.0, 0.0);
ev_timer_start (loop, timer)
这样的设置,当每次有活跃时间时,停止timer,并且重启它。第一个参数是首次超时,第二个参数是第二次开始的固定超时时间。
但是这样的方法虽然比较简易,但是时间不稳定,而且开销较大
2. 使用ev_timer_again
重设
使用ev_timer_again
,可以忽略ev_timer_start
ev_init (timer, callback);
timer->repeat = 60.0;
ev_timer_again (loop, start);
上面的初始化完成后,在 callback 里调用:
timer->repeat = 60.0;
ev_timer_again (loop, timer);
可以改变 timeout 值,不管 timer 是否 active
3. 让 timer 超时,但视情况重新配置
这个方式的基本思路是因为许多 timeout 时间都比 interval 大很多,此时要记住上一次活跃的时间,然后再 callback 中检查真正的 timeout
ev_tstamp g_timeout = 60.0;
ev_tstamp g_last_activity;
ev_timer g_timer;
static void callback (EV_P_ev_timer *w, int revents)
{
ev_tstamp after = g_last_activity - ev_now(EV_A) + g_timeout;
// 如果小于零,表示时间已经发生了,已超时
if (after < 0.0) {
...... // 执行 timeout 操作
}
else {
// callback 被调用了,但是却有一些最近的活跃操作,说明未超时
// 此时就按照需要设置的新超时事件来处理
ev_timer_set (w, after, 0.0);
ev_timer_start (loop, g_timer);
}
}
启用这种模式,记得初始化时将g_last_activity
设置为ev_now
,并且调用一次callback (loop, &g_timer, 0)
;当活跃时间到来时,只需修改全局的 timeout 变量即可,然后再调用一次 callback
g_timeout = new_value
ev_timer_stop (loop, &timer)
callback (loop, &g_timer, 0)
4. 为 timer 使用双向链表
使用场景:有成千上万个请求,并且都需要 timeouts
当 timeout 开始前,计算 timeout 的值,并且将 timeout 放在链表末尾。然后当链表前面的项需要 fire 时。使用ev_timer
来将其 fire 掉。
当有 activity 时,将 timer 从 list 中一处,重算 timeout,并且再附到 list 末尾,确保如果ev_timer
已经被 list 的第一项取出时,更新它
“太早”的问题
假设在500.9秒的时候请求延时1秒,那么当501秒到来时,可能导致 timeout,这就是“太早”问题。Libev的策略是对于这种情况,在502秒时才执行 timeout。但是这又有“太晚”的问题,请程序员注意.
“假死”问题
Suspenged animation,也称为休眠,指的是将机子置于休眠状态。注意不同的机子不同的系统这个行为可能不一样。
其中一种休眠是使得所有程序感觉只是经过了很小的一段时间一般(时间跳跃)
推荐在SIGTSTP
处理中调用ev_suspend
和ev_resume
其他注意点
ev_now_update()
的开销很大,请谨慎使用
Libev使用的时一个内部的单调时钟而不是系统时钟,而ev_timer
则是基于系统时钟的,所以在做比较的时候两者不同步。
相关函数
void ev_timer_init (ev_timer *, callback, ev_tstamp after, ev_tstamp repeat);
void ev_timer_set (ev_timer *, ev_tstamp after, ev_tstamp repeat);
如果repeat为正,这个timer会重复触发,否则只触发一次。
void ev_timer_again (loop, ev_timer *)
ev_tstamp ev_timer_remaining (loop, ev_timer *)
ev_periodic:基于日历的定时器
相关函数
void ev_periodic_init (ev_periodic *, callback, ev_tstamp offset,
ev_tstamp interval, reschedule_cb)
void ev_periodic_set (ev_periodic *, ev_tstamp offset,
ev_tstamp interval, reschedule_cb)
以下是几种不同应用场景的设置方法:
- 绝对计时器:offset 等于绝对时间,interval 为0,reschedule_cb 为 NULL。在这种设置下,时钟只执行一次,不重复
- 重复内部时钟:offset 小于等于 interval 值,interval 大于0,reschedule_cb 为 NULL。这种设置下,watcher 永远在每一个(offset + N * interval)超时。
- 手动排程模式:offset 忽略,reschedule_cb 设置。使用 callback 来返回下次的 trigger 时间。callback 原型为:
ev_tstamp (*reschedule_cb)(ev_periodic *w, ev_tstamp now);
例程是:
static ev_tstamp my_scheduler (...)
{
return now + 60.0;
}
类似于 Linux 内核的jiffies
,返回下一个时间点。
这个timer非常便于用来提供诸如“下一个正午12点”之类的定时器。
void ev_periodic_again (loop, ev_periodic *)
关闭并重启 watcher,参见前文。
ev_tstamp ev_periodic_at (ev_periodic *)
返回下一次触发的绝对时间。
ev_signal:捕获 signal 事件
在哦你跟一个 loop 可以多次观测同一个 signal,但是无法在多个 loop 中观测同一个 signal。此外,SIGCHILD
只能在 default loop 中监听。
注意点
关于继承 fork / execve / ptherad_create 的问题
在子进程调用 exec
之前,应当将 signal mask 重设为你所需的默认值。最简单的方法就是子进程做一个pthread_atfork()
来重设。
关于线程信号处理
POSIX 的不少功能(如sigwait)只有在进程中的所有线程屏蔽了 signal 时才真正生效
为了解决这个问题,如果真的要使用这些功能的话,建议在创建线程之前屏蔽所有的 signal,并且在创建 loops 的时候指定EVFLAG_NOSIGMASK
,然后制定一个 thread 用来接收 signals。
相关函数
void _ev_signal_init (ev_signal *, callback, int signum)
void ev_signal_set (ev_signal *, int signum)
ev_child:子进程退出事件
当接收到SIGCHILD
事件时,child watcher 触发。大部分情况下,子进程退出或被杀掉。只要这个 watcher 的 loop 未开始,你甚至可以在 shild 被 fork 之后才加入 child watcher。
Ev_child 的优先级固定是EV_MAXPRI
。
void ev_chile_init (ev_child *, callback, int pid, int trace)
void ev_child_set (ev_child *, int pid, int trace)
Pid 如果指定0的话,表示任意子进程。可以在 ev_child 中观察rstatus
成员来了解子进程状态。
int pid;
表示监控中的 pid,只读。
int rpid;
可读写,表示检测到状态变化的 pid
int tstatus;
可读写,表示由 rpid 导致的进程的 exit/trace 状态值。
ev_stat:监控文件属性变化
使用 ev_stat 时,监控目标位置上无需存在文件,因为文件从“不存在”变为存在也是一种状态变化。
文件路径必须是绝对路径,不能存在“./
”或“../
”。
Ev_stat 的实现其实只是定期调用stat()
来判断文件属性的变化,所以可以指定检查周期。指定0的话会使用默认事件周期。
正因为这是轮询操作,所以这个功能不适合做大数据量或者是大并发检测;同时,ev_stat 是异步的。
大文件支持
默认关闭大文件支持(使用32位的stat
)。如果要使用大文件支持(ABI),libev 的作者在这里吐槽,说你要游说操作系统的发布方去支持……囧rz
关于文件时间
有些系统的文件时间仅精确到秒,这就意味着 ev_stat 无法区分秒以下的变动。
相关函数和数据成员
void ev_stat_init (ev_stat *, callback, const char *path, ev_tstamp interval);
void ev_stat_set (ev_stat *, const char *path, ev_tstamp interval);
void ev_stat_stat (loop, ev_stat *);
第三个函数使用新的文件 stat 值去更新 stat buffer,使用此函数来使得你做的一些配置更改不会被触发。
ev_statdata attr
只读,代表文件最近一次的状态。ev_statdata
和struct stat
基本是相通的。
ev_statdata prev
文件上一次的状态
ev_tstamp interval
const char *path
都是只读,字面意义上的意思。
ev_idle:无事可做时的事件
void ev_idle_init (ev_idle *, callback)
这个功能没有研究过,暂记着把。
其他事件(仅记录)
ev_prepare 和 ev_check
ev_embed
ev_fork
ev_cleanup
ev_asunc
其他函数
void ev_once (loop, int fd, int events, ev_tstamp timeout, callback)
从指定的f fd 中指定一个超时事件,这个函数的方便之处在于无需做 alloc
/conf
/start
/stop
/free
。
Fd 可以小于0,这样就没有 I/O 监控,并且“events”会被忽略。
void ev_feed_event (loop, int fd, int revents);
向一个 fd 发送事件。需要注意的是,这个功能貌似是只能在 loop 内调用才有效,异步地在 loop 的另一个线程直接调用是无效的。
void ev_feed_signal_event (loop, signum)
向一个 loop 模拟 signal。参见 ev_feed_signal
。
系列篇
Libev 官方文档学习笔记(1)——概述和 ev_loop
Libev 官方文档学习笔记(2)——watcher 基础
Libev 官方文档学习笔记(3)——常用 watcher 接口(本文)
使用 libev 构建 TCP 响应服务器的简单流程
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。